package imoney.server.util;

import imoney.shared.exception.AlreadyExistingException;
import imoney.shared.model.DebtManager;
import imoney.shared.model.Group;
import imoney.shared.model.MoneyTransaction;
import imoney.shared.model.Person;
import imoney.shared.model.Registreable;

import java.util.ArrayList;
import java.util.List;

import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.Transaction;

import com.google.appengine.api.datastore.Key;


/**
 * A module class used to interact with the datastore. It provides
 * generic methods to fetch and store instances of models. The aim is
 * to factorize query writing to speed up code writing and ease future
 * potential changes. Also helps debugging.
 * 
 * @author Arnaud PELOQUIN
 *
 */
//  To delete everything in the datastore : find and delete this file : local_db.bin
public class DataStoreManager {
	private static final PersistenceManagerFactory pmf = PersistenceManagerFacto.getPMF();
	private static PersistenceManager pm;
	private static Key idManagerKey;

	//PUBLIC MEHODS --------------------------------------

	/**
	 * Method to call when an object needs to be saved with a unique ID by the app. This method checks that the object is not
	 * already in the database using the object's inner method.
	 * Then it opens a transaction to use IDsManager to get a unique ID and write the object with consistency.
	 * To simply store an object (with no transaction), call makePersistent().<br><br>
	 * 
	 * The persistence manager is not closed after the method returns.
	 */
	public static void registerAndMakePersistent(Registreable object) throws AlreadyExistingException {
		checkPM();
		
		checkAvailability(object);

		setParentKey(object);

		makeTransaction(object);
	}

	/**
	 * Calls <code>PersistenceManager.close()</code>
	 */
	public static void closePM(){
		pm.close();
	}

	
	@SuppressWarnings("unchecked")	
	public static List<String> get5lastTransacs(){
		checkPM();	
		List<MoneyTransaction> transacs;
		String queryString = "select from "+MoneyTransaction.class.getName(); //TODO :+" order by id asc range 0,5";
		transacs = (List<MoneyTransaction>) pm.newQuery(queryString).execute();
		
		List<String> list = new ArrayList<String>();
		for(MoneyTransaction tr : transacs){
			list.add(tr.toString());
		}
		return list;
	}
	
	/**
	 * Fetch in the database and return the person correspondng to the ID.
	 * @param ID : int
	 * @return pers, the first Person corresponding to the adress, or null if nobody matches
	 * the criteria.
	 */
	public static Person getPerson(int ID){
		String query = "select from "+Person.class.getName()+" where id == "+ID;
		return internGetPerson(query);
	}
	
	/**
	 * Fetch in the database and return the person correspondng to the adress.
	 * @param adress : a String equal to the mail adress of the person.
	 * @return pers, the first Person corresponding to the adress, or null if nobody matches
	 * the criteria.
	 */
	public static Person getPerson(String googleUserID){
		String query = "select from "+Person.class.getName()+" where userID == '"+googleUserID+"'";
		return internGetPerson(query);
	}
	
	/**
	 * Fetch in the database and return the person correspondng to the pseudo.
	 * @param pseudo : the pseudo of the person.
	 * @return the person found, or null if not found.
	 */
	public static Person getPersonByPseudo(String pseudo){
		String query = "select from "+Person.class.getName()+" where pseudo == '"+pseudo+"'";
		return internGetPerson(query);
	}
	
	/**
	 * Returns the first group corresponding to the ID
	 * @param name : the name of the person
	 * @return group : the requested group if found, null otherwise.
	 */
	public static Group getGroup(String name){
		checkPM();
		String query = "select from "+Group.class.getName()+" where name == '"+name+"'";
		@SuppressWarnings("unchecked")
		List<Group> groups = (List<Group>) pm.newQuery(query).execute();
		if(!groups.isEmpty()){
			return groups.get(0);
		}
		return null;
	}
	
	/**
	 * Fetches in the database the groups matching the given names.
	 * @param names : the names of the groups to find.
	 * @return groups : the list of found groups. List can be empty. List cannot contain null.
	 */
	public static ArrayList<Group> getGroupList(ArrayList<String> names) {
		checkPM();
		ArrayList<Group> groups = new ArrayList<Group>();
		Transaction tx = pm.currentTransaction();
		tx.begin();
		try {
			for(String name : names) {
				// not really nice because the checkPM() is re-performed inside the loop...
				Group gp = getGroup(name);
				if(gp != null) {
					groups.add(gp);
				}
				else {
					System.err.println(DataStoreManager.class.getName()+", Group not found : "+name);
				}
			}
		}
		finally {
			if(tx.isActive()) {
				tx.rollback();
			}
		}
		return groups;
	}
	
	/**
	 * Returns the DebtManager Corresponding to the ID, or null if no DebtManager matches the ID.
	 * @param id : the integer id of the debtmanager
	 * @return debtManager, the DebtManager found in the database.
	 */
	public static DebtManager getDebtManager(int id){
		checkPM();
		String query = "select from "+DebtManager.class.getName()+" where id == "+id;
		@SuppressWarnings("unchecked")
		List<DebtManager> debtManagers = (List<DebtManager>) pm.newQuery(query).execute();
		if(!debtManagers.isEmpty()){
			return debtManagers.get(0);
		}
		return null;
	}

	/**
	 * Updates the pseudo of the person in the database, and
	 * updates the context too, so the result is visibile.
	 * @param googleID : the userID of the person.
	 * @param pseudo : the new pseudo to set.
	 * @return the boolean indicating if the operation was successful.
	 */
	public static boolean updatePseudo(String googleID, String pseudo) {
		// TODO : might be compulsory to check redundant pseudo (if identification based on it)
		checkPM();
		Transaction tx = pm.currentTransaction();
		try {
			tx.begin();
			Person pers = getPerson(googleID);
			pers.setPseudo(pseudo);
			tx.commit();
		}
		finally {
			if(tx.isActive()) {
				tx.rollback();
			}
			pm.close();
			}
		return true;
	}
	
	/**
	 * Deprecated because it has to be rebuilt more smart.<br><br>
	 * Updates this group in the database if it exists, if not do nothing. <br>
	 * WARNING : for now the member list is simply replaced by the new one.
	 * Existing debts concerning deleted member should bug and create fail.
	 * @param group : the group to update.
	 * @return success : the boolean indicating the success of the operation.
	 */
	/*
	 *  TODO : complete this method with associated debtmanager update,
	 *  and warn user if existing debt are gonna be erased when trying to
	 *  delete someone from the group.
	 */
	@Deprecated
	public static boolean updateGroup(Group group) {
		boolean success = false;
		checkPM();
		try {
			checkAvailability(group);
		}
		catch (AlreadyExistingException ex) {
			Transaction transaction = pm.currentTransaction();
			transaction.begin();
			try {
				Group gp = getGroup(group.getName());
				gp.setMembres(group.getMembres());
				gp.setBanker(group.getBanker());
				success = true;
				transaction.commit();
			}
			finally {
				if (transaction.isActive()) {
					transaction.rollback();
				}
				closePM();
			}
		}
		return success;
	}

	// PRIVATE METHODS ---------------------------------------------------------

	/**
	 * A short method checking if the pm is still valid. The objective is to use a persistence manager (pm) multiple times if needed,
	 * and close it after a series of operations in other classes. So pm closing is public, but the opening is done
	 * only if needed, hence the private checkPM() method that should be used in each method
	 */
	private static void checkPM(){
		if(pm==null || pm.isClosed()){
			pm=pmf.getPersistenceManager();
		}
	}

	/**
	 * Method that terminates silently if the object is not present in the database,
	 * but throws an exception if there is already a copy of the object saved.
	 * @param object : the object to check availability.
	 * @throws AlreadyExistingException if the object is already stored in the database.
	 */
	private static void checkAvailability(Registreable object) throws AlreadyExistingException{
		String query = object.getAvailabilityQuery();
		@SuppressWarnings("unchecked")
		List<Registreable> list = (List<Registreable>) pm.newQuery(query).execute();
		if(!list.isEmpty()){
			throw new AlreadyExistingException(object.getAvailabilityExceptionMessage());
		}
	}

	/**
	 * Parent Key setting to group the object and idManager in the same entity, in order to create a transaction
	 * See  http://code.google.com/appengine/docs/java/datastore/transactions.html   for more details
	 * @param object
	 */
	@SuppressWarnings("unchecked")
	private static void setParentKey(Registreable object){
		if(idManagerKey != null){
			object.setParentKey(idManagerKey);
		}
		else{
			List<IDsManager> idmanagerList =(List<IDsManager>) pm.newQuery("select from "+IDsManager.class.getName()).execute();
			idManagerKey = idmanagerList.get(0).getKey();
			object.setParentKey(idManagerKey);
		}
	}
	/**
	 * WARNING : not to use without setting the parent key to the object !
	 * Create a transaction to save the object and increment ids with consistency. The atempt will be made several time
	 * in case of transaction rollback (failure of one ore more operation inside the transaction).
	 * @param pers
	 */
	private static void makeTransaction(Registreable object){
		Transaction tx = pm.currentTransaction();
		try{
			tx.begin();
			IDsManager idmanager = (IDsManager) pm.getObjectById(IDsManager.class,idManagerKey);
			object.setId(idmanager.getNextID(object));
			pm.makePersistent(idmanager);
			pm.makePersistent(object);
			tx.commit();
		}
		finally{
			if(tx.isActive()){
				tx.rollback();
			}
		}
	}

	/**
	 * internal method that enables to fetch user either by ID or adress depending on the query.
	 * If no user is registered that correspond to that query, null is returned.
	 * @param query : the query that will be sent to the Datastore
	 * @return the person corresponding to the query, or null if there is no match
	 */
	private static Person internGetPerson(String query){
		// TODO : recheck this method a cause du deplacemen de Person dans shared qui a foutu la merde
		checkPM();
		@SuppressWarnings("unchecked")
		List<Person> list = (List<Person>) pm.newQuery(query).execute();
		
		if(!list.isEmpty()){
			// TODO : check redondancy while fetching the person ? Maybe not for every query, but why not
			return list.get(0);
		}
		return null;
	}
}





















